﻿using System;
using System.Web;
using System.Xml;
using Selenium;

namespace Quail
{
    /// <summary>
    /// Implements access to the web browser through Selenium calls
    /// </summary>
    public abstract class SimpleBrowser : IWebBrowser, IDisposable
    {
        private int _timeoutI;
        private string _cachedLocation;
        private int _cacheCount;
        protected Func<string, string> GlobalUrlModification = null; 
        
        /// <summary>
        /// Initializes a new instance of the WebBrowser class (this does NOT create a new browser window!)
        /// </summary>
        /// <param name="ts">The test session</param>
        protected SimpleBrowser(TestSession ts)
            : this(ts, null, null)
        {
        }

        /// <summary>
        /// Initializes a new instance of the WebBrowser class  (this does NOT create a new browser window!)
        /// </summary>
        /// <param name="ts">The test session</param>
        /// <param name="page">The web page class</param>
        protected SimpleBrowser(TestSession ts, IWebPage page)
            : this(ts, page, null)
        {
        }

        /// <summary>
        /// Initializes a new instance of the WebBrowser class  (this does NOT create a new browser window!)
        /// </summary>
        /// <param name="ts">The test session</param>
        /// <param name="page">The web page class</param>
        /// <param name="cookie">The cookie object</param>
        protected SimpleBrowser(TestSession ts, IWebPage page, IWebCookie cookie)
        {
            TestSession = ts;
            GenericPage = page;
            Cookie = cookie;
            Timeout = ts.DefaultBrowserTimeout;
        }

        ~SimpleBrowser()
        {
            Dispose(false);
        }

        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }

        protected virtual void Dispose(bool disposing)
        {
        }

        /// <summary>
        /// Setup a function that will modify the url before navigation
        /// </summary>
        /// <param name="func">A function accepting and returning a string (url)</param>
        public void SetUrlModification(Func<String, String> func)
        {
            GlobalUrlModification = func;
        }
        
        /// <summary>
        /// Gets or sets the WebPage for the Browser
        /// </summary>
        public IWebPage GenericPage
        {
            get;
            set;
        }

        /// <summary>
        /// Gets the current test session
        /// </summary>
        public TestSession TestSession
        {
            get;
            private set;
        }

        /// <summary>
        /// Gets the Cookie object for the Browser
        /// </summary>
        public IWebCookie Cookie
        {
            get;
            protected set;
        }

        /// <summary>
        /// Gets or sets the number of milliseconds for timeouts (default: 60000)
        /// </summary>
        public int Timeout
        {
            get
            {
                return _timeoutI;
            }

            set
            {
                SetTimeout(value);
            }
        }

        /// <summary>
        /// Called to set the timeout for the browser
        /// </summary>
        /// <param name="timeout"></param>
        protected virtual void SetTimeout(int timeout)
        {
            if (_timeoutI != timeout)
            {
                _timeoutI = timeout;
                ActiveTask.AddLog(LogStatus.Information, "SimpleBrowser.Timeout set to {0}", _timeoutI);
            }
        }
       
        /// <summary>
        /// Gets an interface to Selenium or a Selenium-like implementation
        /// </summary>
        public abstract ISelenium SeleniumInterface { get; }

        /// <summary>
        /// Gets or sets the selenium run speed in milliseconds
        /// </summary>
        public virtual int RunSpeed { get; set; }

        /// <summary>
        /// Gets the current ActiveTask for the test
        /// </summary>
        public Task ActiveTask
        {
            get
            {
                return TestSession.ActiveTask;
            }
        }

        /// <summary>
        /// Gets or sets a value indicating whether successful Browser task are "Rolledback" from the logging system.
        /// </summary>
        public bool SilentLogging { get; set; }

        /// <summary>
        /// Gets the URL of the current web page
        /// </summary>
        public string Location
        {
            get
            {

                if (_cacheCount > 0)
                {
                    if (_cachedLocation == null)
                        _cachedLocation = GetLocation();

                    return _cachedLocation;
                }

                return GetLocation();
            }
        }

        /// <summary>
        /// Implementation of the Location property
        /// </summary>
        /// <returns>The current browser location</returns>
        protected abstract string GetLocation();

        /// <summary>
        /// Gets the location URI.
        /// </summary>
        /// <value>The location URI.</value>
        public Uri LocationUri
        {
            get
            {
                return new Uri(Location, UriKind.Absolute);
            }
        }

        /// <summary>
        /// Cache the location
        /// </summary>
        private void CacheLocationStart()
        {
            _cacheCount++;
        }

        /// <summary>
        /// Un cache the location
        /// </summary>
        private void CacheLocationEnd()
        {
            _cacheCount--;

            if (_cacheCount == 0)
                _cachedLocation = null;

            if (_cacheCount < 0)
                throw new Exception("Invalid usage of CacheLocation/UnCacheLocation methods in WebBrowser");
        }

        /// <summary>
        /// Returns a disposable object used to cache the current web page location
        /// </summary>
        /// <returns></returns>
        public IDisposable CacheLocation()
        {
            return new BrowerCacheLocation(this);
        }

        /// <summary>
        /// Class to create to cache the browser location for performance reasons
        /// </summary>
        public class BrowerCacheLocation : IDisposable
        {
            private SimpleBrowser _browser;

            /// <summary>
            /// Initializes a new instance of the <see cref="BrowerCacheLocation"/> class.
            /// </summary>
            /// <param name="browser">The browser.</param>
            internal BrowerCacheLocation(SimpleBrowser browser)
            {
                _browser = browser;
                _browser.CacheLocationStart();
            }

            ~BrowerCacheLocation()
            {
                Dispose(false);
            }

            public void Dispose()
            {
                Dispose(true);
                GC.SuppressFinalize(this);
            }

            protected virtual void Dispose(bool disposing)
            {
                _browser.CacheLocationEnd();
                _browser = null;
            }
        }

        /// <summary>
        /// Clicks the back button in the browser. Syncs any page
        /// </summary>
        public void Back()
        {
            Back(GenericPage);
        }

        /// <summary>
        /// Clicks the back button in the browser. Syncs the passed page
        /// </summary>
        /// <param name="page">The web page to sync against</param>
        /// <param name="ysod">How to check for the ysod</param>
        public void Back(IWebPage page, YSODCheckTypes ysod = YSODCheckTypes.CheckForYSOD)
        {
            ActiveTask
                .RunTask(
                    "SimpleBrowser.Back",
                    task =>
                        {
                            PerformBack();
                            Sleep(250);
                            page.Sync(ysod);

                            if (SilentLogging)
                                task.Rollback();
                        });
        }

        /// <summary>
        /// Called by Back() method to back the web page
        /// </summary>
        protected abstract void PerformBack();

        /// <summary>
        /// Returns a stopwatch for timeouts
        /// </summary>
        /// <returns>A valid stopwatch</returns>
        public TestStopwatch GetStopwatch()
        {
            return GetStopwatch(Timeout);
        }

        /// <summary>
        /// Returns a stopwatch for timeouts
        /// </summary>
        /// <param name="ms">The number of milliseconds for the stopwatch to expire</param>
        /// <returns>A valid stopwatch</returns>
        public TestStopwatch GetStopwatch(int ms)
        {
            return new TestStopwatch(ms == -1 ? Timeout : ms);
        }

        /// <summary>
        /// Navigates to a specific url that is not on the "testing" site [obsolete]
        /// </summary>
        /// <param name="url">A valid URL</param>
        [Obsolete("Please use Navigate(url) instead")]
        public void OffSiteNavigate(string url)
        {
            Navigate(url);
        }

        /// <summary>
        /// Navigates to a url on the current site
        /// </summary>
        /// <param name="url">A url local to the current site</param>
        /// <param name="ysodCheck">The YSOD check flag</param>
        public void Navigate(string url, YSODCheckTypes ysodCheck = YSODCheckTypes.CheckForYSOD)
        {
            Navigate(url, GenericPage, ysodCheck);
        }

        /// <summary>
        /// Navigates to a url on the current site
        /// </summary>
        /// <param name="url">A url local to the current site</param>
        /// <param name="timeoutTryCounter">Number of times to try to load the page (it'll navigate up to this many times)</param>
        /// <param name="ysodCheck">The YSOD check flag</param>
        public void NavigateWithTimeoutRetry(string url, int timeoutTryCounter = 3, YSODCheckTypes ysodCheck = YSODCheckTypes.CheckForYSOD)
        {
            WaitParameters.RetryTimeout(
                ActiveTask,
                "NavigateWithTimeoutRetry",
                t => Navigate(url, GenericPage, ysodCheck),
                timeoutTryCounter
                );
        }

        /// <summary>
        /// Gets the Network Traffic Logger (throws NotImplementedException)
        /// </summary>
        public virtual NetworkTraffic NetworkTrafficLogger
        {
            get { throw new NotSupportedException(); }
        }

        /// <summary>
        /// Navigates to a url on the current site - use OffSiteNavigate to navigate to a differet web site
        /// </summary>
        /// <param name="url">A url local to the current site</param>
        /// <param name="page">The page to sync against after the navigation</param>
        /// <param name="ysodCheck">The YSOD check flag</param>
        public void Navigate(string url, IWebPage page, YSODCheckTypes ysodCheck = YSODCheckTypes.CheckForYSOD)
        {
            var originalurl = url;
            ActiveTask
                .RunTask(
                    "SimpleBrowser.Navigate",
                    task =>
                        {
                            task.AddAttribute("NavigateTo", originalurl);
                            if (GlobalUrlModification != null)
                                url = GlobalUrlModification(url);

                            var uri = new Uri(url, UriKind.RelativeOrAbsolute);

                            if (!uri.IsAbsoluteUri)
                            {
                                if (url.StartsWith("//"))
                                {
                                    // URLs that start with a double slash just need the scheme added to the front of them.
                                    // these guys should inherit http or https from the current location.
                                    var currenturi = new Uri(Location, UriKind.Absolute);
                                    url = String.Format("{0}:{1}", currenturi.Scheme, url);
                                }
                                else
                                {

                                    var baseuri = new Uri(TestSession.BaseUrl, UriKind.Absolute);
                                    var baseurl = baseuri.GetLeftPart(UriPartial.Path);

                                    if (!baseurl.EndsWith("/") && !url.StartsWith("/"))
                                        url = baseurl + "/" + url;
                                    else if (baseurl.EndsWith("/") && url.StartsWith("/"))
                                        url = baseurl + url.Substring(1);
                                    else
                                        url = baseurl + url;
                                }
                            }

                            PerformNavigate(url);

                            page.Sync(ysodCheck);

                            if (SilentLogging)
                                task.Rollback();
                        });
        }

        /// <summary>
        /// Navigates to a given url
        /// </summary>
        /// <param name="url">The web address to navigate to</param>
        protected abstract void PerformNavigate(string url);

        /// <summary>
        /// Reloads the current browser page. Sync any page
        /// </summary>
        /// <param name="ysod">The YSOD check type</param>
        public void Refresh(YSODCheckTypes ysod = YSODCheckTypes.CheckForYSOD)
        {
            Refresh(GenericPage, ysod);
        }

        /// <summary>
        /// Reloads the current browser page
        /// </summary>
        /// <param name="page">The web page to sync against</param>
        /// <param name="ysod">The YSOD check type</param>
        public void Refresh(IWebPage page, YSODCheckTypes ysod = YSODCheckTypes.CheckForYSOD)
        {
            ActiveTask.RunTask(
                "SimpleBrowser.Refresh",
                task =>
                    {
                        PerformRefresh();
                        page.Sync(ysod);
                        if (SilentLogging)
                            task.Rollback();
                    });
        }

        /// <summary>
        /// Reloads the current browser page
        /// </summary>
        protected abstract void PerformRefresh();

        /// <summary>
        /// Waits for the specified time
        /// </summary>
        /// <param name="milliseconds">The number of milliseconds to wait</param>
        public void Sleep(int milliseconds)
        {
            System.Threading.Thread.Sleep(milliseconds);
        }

        /// <summary>
        /// Opens an XML url and returns a DOM with the contents - an exception will be thrown if the Xml url is invalid or empty
        /// </summary>
        /// <param name="url">the url to open</param>
        /// <returns>The XmlDocument</returns>
        public XmlDocument GetUrlContentAsXml(string url)
        {
            return ActiveTask
                .RunTask(
                    "SimpleBrowser.GetUrlContentAsXml",
                    task =>
                        {
                            task.AddAttribute("Url", url);
                            var dom = new XmlDocument();
                            dom.LoadXml(GetUrlContent(url));

                            if (SilentLogging)
                                task.Rollback();

                            return dom;
                        });
        }

        /// <summary>
        /// Opens an XML url and returns a DOM with the contents - an exception will be thrown if the Xml url is invalid or empty
        /// </summary>
        /// <param name="url">the url to open</param>
        /// <returns>The XmlDocument</returns>
        public string GetUrlContent(string url)
        {
            return ActiveTask
                .RunTask(
                    "SimpleBrowser.GetUrlContent",
                    task =>
                        {
                            task.AddAttribute("Url", url);
                            var text = PerformGetUrlContent(url);

                            if (SilentLogging)
                                task.Rollback();

                            return text;
                        });
        }

        protected abstract string PerformGetUrlContent(string url);

        /// <summary>
        /// Encodes a string for a URL
        /// </summary>
        /// <param name="str">the string to encode</param>
        /// <returns>the encoded string</returns>
        public static string UrlEncode(string str)
        {
            return HttpUtility.UrlEncode(str);
        }

        /// <summary>
        /// Decodes a string for a URL
        /// </summary>
        /// <param name="str">the string to decode</param>
        /// <returns>the decoded string</returns>
        public static string UrlDecode(string str)
        {
            return HttpUtility.UrlDecode(str);
        }

        /// <summary>
        /// Waits for waitDelegate method to return true using the Browser's default timeout.
        /// </summary>
        /// <param name="waitParameters">wait parameters</param>
        /// <returns>True if the wait succeeded (depending on waitparameters ThrowOnTimeout flag!)</returns>
        /// <exception cref="WebBrowserException">Thrown if the page is not found within the default timeout</exception>
        [Obsolete("Use WaitParameters.Wait(ActiveTask) instead")]
        public bool Wait(WaitParameters waitParameters)
        {
            return waitParameters.Wait(ActiveTask);
        }

        /// <summary>
        /// Syncs against any page.
        /// </summary>
        /// <param name="ysod">The YSOD check type</param>
        public void SyncAnyPage(YSODCheckTypes ysod = YSODCheckTypes.CheckForYSOD)
        {
            GenericPage.SyncAnyPage(ysod);
        }

        /// <summary>
        /// Scroll the browser to the position of a <see cref="WebControl"/> so that it is visible.
        /// </summary>
        /// <param name="control">The <see cref="WebControl"/> to scroll to.</param>
        public void ScrollTo(WebControl control)
        {
            SeleniumInterface.RunScript(string.Format("window.scrollTo({0}, {1});", control.Left, control.Top));
        }
    }
}
